Skip to main content

Java Main Method & JVM Memory Model

Table of Contents

  1. The main Method Deep Dive
  2. JVM Execution Process
  3. JVM Memory Areas
  4. Multithreading & Memory Model

1. The main Method Deep Dive

Why is main Special?

The main method is the entry point of any standalone Java application. When you run java MyClass, the JVM specifically looks for this exact signature:

public static void main(String[] args)

If missing: JVM throws Error: Main method not found in class MyClass

Breakdown of Each Keyword

1. public

  • Purpose: JVM must access it from outside the class
  • Impact: If private or protected → runtime error
// ❌ This won't work
private static void main(String[] args) {
System.out.println("Hello World");
}
// JVM cannot access private methods

2. static

  • Purpose: JVM calls main without creating class instance
  • Impact: Belongs to class, not instance
// ✅ JVM can call directly
public class MyClass {
public static void main(String[] args) {
// No need for: MyClass obj = new MyClass();
System.out.println("Hello World");
}
}

3. void

  • Purpose: main doesn't return anything
  • Impact: JVM just exits after execution
// ❌ JVM wouldn't know what to do with return value
public static int main(String[] args) {
return 42; // What should JVM do with this?
}

4. main

  • Purpose: Method name JVM is hardcoded to find
  • Impact: Execution always starts from main

5. String[] args

  • Purpose: Holds command-line arguments
  • Variations allowed:
    • String args[]
    • String... args (varargs)
public class Demo {
public static void main(String[] args) {
System.out.println("Args length: " + args.length);
if (args.length > 0) {
System.out.println("First arg: " + args[0]);
}
}
}

// Run: java Demo hello world
// Output:
// Args length: 2
// First arg: hello

Method Overloading with main

public class MainOverload {
// ✅ JVM will call this one
public static void main(String[] args) {
System.out.println("Main with String[] args");
main("custom"); // Can call overloaded version
}

// ✅ Valid overload, but JVM won't call it directly
public static void main(String arg) {
System.out.println("Overloaded main: " + arg);
}
}

2. JVM Execution Process

Complete Flow: From Source to Execution

Your Code (.java)
↓ javac (compilation)
Bytecode (.class)
↓ java (execution)
JVM → Class Loader → Bytecode Verifier → Execution Engine

Calls public static void main(String[] args)

Your Program Runs 🎉

Step-by-Step Process

Step 1: Compilation

javac MyClass.java
# Creates MyClass.class (platform-independent bytecode)

Step 2: Execution Launch

java MyClass hello world
# Launches JVM with command-line arguments

Step 3: Class Loading

// Class Loader Subsystem loads classes in this order:
// 1. BootStrap Loader → core Java classes (java.lang.String)
// 2. Extension Loader → extended libraries
// 3. Application Loader → your classes (MyClass)

Step 4: JVM Verification

  • Bytecode Verifier checks for:
    • Legal bytecode instructions
    • No memory access violations
    • Type safety compliance

Step 5: Execution Engine

  • Interpreter: Executes instructions line by line
  • JIT Compiler: Converts frequently used code to native machine code

Step 6: Finding & Calling main

// JVM searches for exact signature
public static void main(String[] args)

// If found, JVM calls:
MyClass.main(new String[]{"hello", "world"});

3. JVM Memory Areas

Memory Structure Overview

┌─────────────────────────────────────────────────────────┐
│ JVM Memory │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Method Area │ │ Heap │ │Stack(Thread)│ │
│ │(Metaspace) │ │ │ │ │ │
│ │- Class Info │ │- Objects │ │- Local Vars │ │
│ │- Static Vars│ │- String Pool│ │- Method │ │
│ │- Bytecode │ │- Arrays │ │ Frames │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘

Detailed Memory Areas

1. Method Area (Metaspace in Java 8+)

Stores: Class-level data

  • Class definitions and metadata
  • Static variables and methods
  • Method bytecode
  • Constant pool

2. Heap

Stores: Objects and instance data

  • All object instances
  • Instance variables
  • Arrays
  • String Pool (for string literals)

3. Stack (Per Thread)

Stores: Method execution data

  • Local variables
  • Method parameters
  • Return addresses
  • References to heap objects

Memory Example with Code

public class MemoryDemo {
private static int staticVar = 100; // Method Area

public static void main(String[] args) {
int x = 10; // Stack
String s1 = "Hello"; // String Pool (Heap)
String s2 = new String("World"); // Heap
Person p = new Person("John"); // Heap

processData(x, s1);
}

static void processData(int num, String text) {
// New stack frame created
int result = num * 2; // Stack
System.out.println(text + ": " + result);
}
}

class Person {
private String name; // Instance variable (Heap)

public Person(String name) {
this.name = name;
}
}

Memory Layout for Above Code

Method Area:
├── MemoryDemo class metadata
├── Person class metadata
├── staticVar = 100
└── Method bytecodes (main, processData, Person constructor)

Heap:
├── String Pool: "Hello"
├── new String("World") object
├── Person object { name: "John" }
└── String object "John" (for Person's name)

Stack (main thread):
┌─────────────────────┐
│ processData() frame │
│ - num = 10 │
│ - text → "Hello" │
│ - result = 20 │
└─────────────────────┘
┌─────────────────────┐
│ main() frame │
│ - args[] │
│ - x = 10 │
│ - s1 → "Hello" │
│ - s2 → new "World" │
│ - p → Person object │
└─────────────────────┘

Stack Frame Lifecycle

public class StackExample {
public static void main(String[] args) {
System.out.println("1. main() starts - frame pushed");
methodA();
System.out.println("4. Back in main() - methodA frame popped");
}

static void methodA() {
System.out.println("2. methodA() starts - frame pushed");
methodB();
System.out.println("3. Back in methodA() - methodB frame popped");
}

static void methodB() {
System.out.println("3. methodB() executing - top frame");
}
}

// Stack Evolution:
// Step 1: [main()]
// Step 2: [main()] → [methodA()]
// Step 3: [main()] → [methodA()] → [methodB()]
// Step 4: [main()] → [methodA()]
// Step 5: [main()]

4. Multithreading & Memory Model

Thread Memory Isolation

Key Principle: Each thread gets its own stack, but all threads share the same heap and method area.

Thread-1 Stack    Thread-2 Stack    Shared Memory
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│Local vars │ │Local vars │ │ Heap │
│Method frames│ │Method frames│ │ Objects │
│ │ │ │ │ │
└─────────────┘ └─────────────┘ ├─────────────┤
│Method Area │
│ Static vars │
└─────────────┘

Thread Safety Example

public class ThreadSafetyDemo {
private static int sharedCounter = 0; // Shared in Method Area

public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Each thread has its own stack with local variables
for (int i = 0; i < 1000; i++) { // 'i' is thread-local
sharedCounter++; // RACE CONDITION - shared resource
}
};

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);

t1.start();
t2.start();

t1.join();
t2.join();

// Expected: 2000, Actual: Often less due to race condition
System.out.println("Final Counter: " + sharedCounter);
}
}

Race Condition Problem

// What happens during sharedCounter++:
// 1. READ current value from memory
// 2. INCREMENT the value
// 3. WRITE back to memory

// If both threads execute simultaneously:
Thread-1: READ (0)INCREMENT (1)WRITE (1)
Thread-2: READ (0)INCREMENT (1)WRITE (1)
// Result: 1 instead of 2 (lost update)

Solutions to Race Conditions

Solution 1: Synchronized Block

public class SynchronizedDemo {
private static int counter = 0;
private static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) { // Only one thread at a time
counter++;
}
}
};

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);

t1.start();
t2.start();
t1.join();
t2.join();

System.out.println("Final Counter: " + counter); // Always 2000
}
}

Solution 2: AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
private static AtomicInteger counter = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // Atomic operation
}
};

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);

t1.start();
t2.start();
t1.join();
t2.join();

System.out.println("Final Counter: " + counter.get()); // Always 2000
}
}

Memory Model Summary

Memory AreaAccessThread SafetyContains
StackPer-threadThread-safeLocal variables, method parameters
HeapSharedNeeds synchronizationObjects, instance variables
Method AreaSharedNeeds synchronizationStatic variables, class metadata

Key Takeaways

  1. Local variables are automatically thread-safe (stored in individual stacks)
  2. Shared objects in heap require synchronization
  3. Static variables are shared across all threads
  4. Race conditions occur when multiple threads access shared mutable state
  5. Synchronization mechanisms (synchronized, atomic classes) ensure thread safety

Quick Reference

Main Method Checklist

  • public static void main(String[] args)
  • ✅ Exact signature required by JVM
  • ✅ Entry point of application
  • ✅ Can be overloaded but JVM calls String[] version

Memory Areas

  • 🏗️ Method Area: Class definitions, static variables
  • 🏠 Heap: Objects, instance variables (shared)
  • 📚 Stack: Local variables, method frames (per-thread)

Thread Safety Rules

  • 🔒 Stack variables: Thread-safe automatically
  • ⚠️ Heap objects: Need synchronization
  • 🚨 Static variables: Need synchronization